"use strict";
import H from "../../parts/Globals.js";

import U from "../../parts/Utilities.js";
var extend = U.extend,
  isNumber = U.isNumber;

var Annotation = H.Annotation,
  ControlPoint = Annotation.ControlPoint,
  merge = H.merge;

/**
 * @class
 * @extends Annotation
 * @memberOf Annotation
 */
function Measure() {
  Annotation.apply(this, arguments);
}

Annotation.types.measure = Measure;

H.extendAnnotation(
  Measure,
  null,
  /** @lends Annotation.Measure# */
  {
    /**
     * Init annotation object.
     */
    init: function () {
      Annotation.prototype.init.apply(this, arguments);

      this.offsetX = 0;
      this.offsetY = 0;
      this.resizeX = 0;
      this.resizeY = 0;

      this.calculations.init.call(this);
      this.addValues();
      this.addShapes();
    },

    /**
     * Overrides default setter to get axes from typeOptions.
     */
    setClipAxes: function () {
      this.clipXAxis = this.chart.xAxis[this.options.typeOptions.xAxis];
      this.clipYAxis = this.chart.yAxis[this.options.typeOptions.yAxis];
    },

    /**
     * Get measure points configuration objects.
     *
     * @return {Array<Highcharts.MockPointOptions>}
     */
    pointsOptions: function () {
      return this.options.options.points;
    },

    /**
     * Get points configuration objects for shapes.
     *
     * @return {Array<Highcharts.MockPointOptions>}
     */
    shapePointsOptions: function () {
      var options = this.options.typeOptions,
        xAxis = options.xAxis,
        yAxis = options.yAxis;

      return [
        {
          x: this.xAxisMin,
          y: this.yAxisMin,
          xAxis: xAxis,
          yAxis: yAxis,
        },
        {
          x: this.xAxisMax,
          y: this.yAxisMin,
          xAxis: xAxis,
          yAxis: yAxis,
        },
        {
          x: this.xAxisMax,
          y: this.yAxisMax,
          xAxis: xAxis,
          yAxis: yAxis,
        },
        {
          x: this.xAxisMin,
          y: this.yAxisMax,
          xAxis: xAxis,
          yAxis: yAxis,
        },
      ];
    },

    addControlPoints: function () {
      var selectType = this.options.typeOptions.selectType,
        controlPoint;

      controlPoint = new ControlPoint(
        this.chart,
        this,
        this.options.controlPointOptions,
        0
      );

      this.controlPoints.push(controlPoint);

      // add extra controlPoint for horizontal and vertical range
      if (selectType !== "xy") {
        controlPoint = new ControlPoint(
          this.chart,
          this,
          this.options.controlPointOptions,
          1
        );

        this.controlPoints.push(controlPoint);
      }
    },
    /**
     * Add label with calculated values (min, max, average, bins).
     *
     * @param {boolean} resize - the flag for resize shape
     */
    addValues: function (resize) {
      var typeOptions = this.options.typeOptions,
        formatter = typeOptions.label.formatter;

      // set xAxisMin, xAxisMax, yAxisMin, yAxisMax
      this.calculations.recalculate.call(this, resize);

      if (!typeOptions.label.enabled) {
        return;
      }

      if (this.labels.length > 0) {
        this.labels[0].text =
          (formatter && formatter.call(this)) ||
          this.calculations.defaultFormatter.call(this);
      } else {
        this.initLabel(
          extend(
            {
              shape: "rect",
              backgroundColor: "none",
              color: "black",
              borderWidth: 0,
              dashStyle: "dash",
              overflow: "none",
              align: "left",
              vertical: "top",
              crop: true,
              point: function (target) {
                var annotation = target.annotation,
                  chart = annotation.chart,
                  inverted = chart.inverted,
                  xAxis = chart.xAxis[typeOptions.xAxis],
                  yAxis = chart.yAxis[typeOptions.yAxis],
                  top = chart.plotTop,
                  left = chart.plotLeft;

                return {
                  x:
                    (inverted ? top : 10) +
                    xAxis.toPixels(annotation.xAxisMin, !inverted),
                  y:
                    (inverted ? -left + 10 : top) +
                    yAxis.toPixels(annotation.yAxisMin),
                };
              },
              text:
                (formatter && formatter.call(this)) ||
                this.calculations.defaultFormatter.call(this),
            },
            typeOptions.label
          )
        );
      }
    },
    /**
     * Crosshair, background (rect)
     */
    addShapes: function () {
      this.addCrosshairs();
      this.addBackground();
    },

    /**
     * Add background shape.
     */
    addBackground: function () {
      var shapePoints = this.shapePointsOptions();

      if (typeof shapePoints[0].x === "undefined") {
        return;
      }

      this.initShape(
        extend(
          {
            type: "path",
            points: this.shapePointsOptions(),
          },
          this.options.typeOptions.background
        ),
        false
      );
    },

    /**
     * Add internal crosshair shapes (on top and bottom)
     */
    addCrosshairs: function () {
      var chart = this.chart,
        options = this.options.typeOptions,
        point = this.options.typeOptions.point,
        xAxis = chart.xAxis[options.xAxis],
        yAxis = chart.yAxis[options.yAxis],
        inverted = chart.inverted,
        xAxisMin = xAxis.toPixels(this.xAxisMin),
        xAxisMax = xAxis.toPixels(this.xAxisMax),
        yAxisMin = yAxis.toPixels(this.yAxisMin),
        yAxisMax = yAxis.toPixels(this.yAxisMax),
        defaultOptions = {
          point: point,
          type: "path",
        },
        pathH = [],
        pathV = [],
        crosshairOptionsX,
        crosshairOptionsY,
        temp;

      if (inverted) {
        temp = xAxisMin;
        xAxisMin = yAxisMin;
        yAxisMin = temp;

        temp = xAxisMax;
        xAxisMax = yAxisMax;
        yAxisMax = temp;
      }
      // horizontal line
      if (options.crosshairX.enabled) {
        pathH = [
          "M",
          xAxisMin,
          yAxisMin + (yAxisMax - yAxisMin) / 2,
          "L",
          xAxisMax,
          yAxisMin + (yAxisMax - yAxisMin) / 2,
        ];
      }

      // vertical line
      if (options.crosshairY.enabled) {
        pathV = [
          "M",
          xAxisMin + (xAxisMax - xAxisMin) / 2,
          yAxisMin,
          "L",
          xAxisMin + (xAxisMax - xAxisMin) / 2,
          yAxisMax,
        ];
      }

      // Update existed crosshair
      if (this.shapes.length > 0) {
        this.shapes[0].options.d = pathH;
        this.shapes[1].options.d = pathV;
      } else {
        // Add new crosshairs
        crosshairOptionsX = merge(defaultOptions, options.crosshairX);
        crosshairOptionsY = merge(defaultOptions, options.crosshairY);

        this.initShape(
          extend(
            {
              d: pathH,
            },
            crosshairOptionsX
          ),
          false
        );

        this.initShape(
          extend(
            {
              d: pathV,
            },
            crosshairOptionsY
          ),
          false
        );
      }
    },

    onDrag: function (e) {
      var translation = this.mouseMoveToTranslation(e),
        selectType = this.options.typeOptions.selectType,
        x = selectType === "y" ? 0 : translation.x,
        y = selectType === "x" ? 0 : translation.y;

      this.translate(x, y);

      this.offsetX += x;
      this.offsetY += y;

      // animation, resize, setStartPoints
      this.redraw(false, false, true);
    },

    /**
     * Translate start or end ("left" or "right") side of the measure.
     * Update start points (startXMin, startXMax, startYMin, startYMax)
     *
     * @param {number} dx - the amount of x translation
     * @param {number} dy - the amount of y translation
     * @param {number} cpIndex - index of control point
     * @param {number} selectType - x / y / xy
     */
    resize: function (dx, dy, cpIndex, selectType) {
      // background shape
      var bckShape = this.shapes[2];

      if (selectType === "x") {
        if (cpIndex === 0) {
          bckShape.translatePoint(dx, 0, 0);
          bckShape.translatePoint(dx, dy, 3);
        } else {
          bckShape.translatePoint(dx, 0, 1);
          bckShape.translatePoint(dx, dy, 2);
        }
      } else if (selectType === "y") {
        if (cpIndex === 0) {
          bckShape.translatePoint(0, dy, 0);
          bckShape.translatePoint(0, dy, 1);
        } else {
          bckShape.translatePoint(0, dy, 2);
          bckShape.translatePoint(0, dy, 3);
        }
      } else {
        bckShape.translatePoint(dx, 0, 1);
        bckShape.translatePoint(dx, dy, 2);
        bckShape.translatePoint(0, dy, 3);
      }

      this.calculations.updateStartPoints.call(
        this,
        false,
        true,
        cpIndex,
        dx,
        dy
      );

      this.options.typeOptions.background.height = Math.abs(
        this.startYMax - this.startYMin
      );

      this.options.typeOptions.background.width = Math.abs(
        this.startXMax - this.startXMin
      );
    },
    /**
     * Redraw event which render elements and update start points
     * if needed
     *
     * @param {Boolean} animation
     * @param {Boolean} resize - flag if resized
     * @param {Boolean} setStartPoints - update position of start points
     */
    redraw: function (animation, resize, setStartPoints) {
      this.linkPoints();

      if (!this.graphic) {
        this.render();
      }

      if (setStartPoints) {
        this.calculations.updateStartPoints.call(this, true, false);
      }

      // #11174 - clipBox was not recalculate during resize / redraw
      if (this.clipRect) {
        this.clipRect.animate(this.getClipBox());
      }

      this.addValues(resize);
      this.addCrosshairs();
      this.redrawItems(this.shapes, animation);
      this.redrawItems(this.labels, animation);

      // redraw control point to run positioner
      this.controlPoints.forEach(function (controlPoint) {
        controlPoint.redraw();
      });
    },
    translate: function (dx, dy) {
      this.shapes.forEach(function (item) {
        item.translate(dx, dy);
      });

      this.options.typeOptions.point.x = this.startXMin;
      this.options.typeOptions.point.y = this.startYMin;
    },
    calculations: {
      /**
       * Set starting points
       * @private
       */
      init: function () {
        var options = this.options.typeOptions,
          chart = this.chart,
          getPointPos = this.calculations.getPointPos,
          inverted = chart.inverted,
          xAxis = chart.xAxis[options.xAxis],
          yAxis = chart.yAxis[options.yAxis],
          bck = options.background,
          width = inverted ? bck.height : bck.width,
          height = inverted ? bck.width : bck.height,
          selectType = options.selectType,
          top = inverted ? chart.plotLeft : chart.plotTop,
          left = inverted ? chart.plotTop : chart.plotLeft;

        this.startXMin = options.point.x;
        this.startYMin = options.point.y;

        if (isNumber(width)) {
          this.startXMax = this.startXMin + width;
        } else {
          this.startXMax = getPointPos(
            xAxis,
            this.startXMin,
            parseFloat(width)
          );
        }

        if (isNumber(height)) {
          this.startYMax = this.startYMin - height;
        } else {
          this.startYMax = getPointPos(
            yAxis,
            this.startYMin,
            parseFloat(height)
          );
        }

        // x / y selection type
        if (selectType === "x") {
          this.startYMin = yAxis.toValue(top);
          this.startYMax = yAxis.toValue(top + yAxis.len);
        } else if (selectType === "y") {
          this.startXMin = xAxis.toValue(left);
          this.startXMax = xAxis.toValue(left + xAxis.len);
        }
      },
      /**
       * Set current xAxisMin, xAxisMax, yAxisMin, yAxisMax.
       * Calculations of measure values (min, max, average, bins).
       * @private
       * @param {Boolean} resize - flag if shape is resized
       */
      recalculate: function (resize) {
        var calc = this.calculations,
          options = this.options.typeOptions,
          xAxis = this.chart.xAxis[options.xAxis],
          yAxis = this.chart.yAxis[options.yAxis],
          getPointPos = this.calculations.getPointPos,
          offsetX = this.offsetX,
          offsetY = this.offsetY;

        this.xAxisMin = getPointPos(xAxis, this.startXMin, offsetX);
        this.xAxisMax = getPointPos(xAxis, this.startXMax, offsetX);
        this.yAxisMin = getPointPos(yAxis, this.startYMin, offsetY);
        this.yAxisMax = getPointPos(yAxis, this.startYMax, offsetY);

        this.min = calc.min.call(this);
        this.max = calc.max.call(this);
        this.average = calc.average.call(this);
        this.bins = calc.bins.call(this);

        if (resize) {
          this.resize(0, 0);
        }
      },
      /**
       * Set current xAxisMin, xAxisMax, yAxisMin, yAxisMax.
       * Calculations of measure values (min, max, average, bins).
       * @private
       * @param {Object} axis - x or y axis reference
       * @param {Number} value - point's value (x or y)
       * @param {Number} offset - amount of pixels
       */
      getPointPos: function (axis, value, offset) {
        return axis.toValue(axis.toPixels(value) + offset);
      },
      /**
       * Update position of start points
       * (startXMin, startXMax, startYMin, startYMax)
       * @private
       * @param {Boolean} redraw - flag if shape is redraw
       * @param {Boolean} resize - flag if shape is resized
       * @param {Boolean} cpIndex - index of controlPoint
       */
      updateStartPoints: function (redraw, resize, cpIndex, dx, dy) {
        var options = this.options.typeOptions,
          selectType = options.selectType,
          xAxis = this.chart.xAxis[options.xAxis],
          yAxis = this.chart.yAxis[options.yAxis],
          getPointPos = this.calculations.getPointPos,
          startXMin = this.startXMin,
          startXMax = this.startXMax,
          startYMin = this.startYMin,
          startYMax = this.startYMax,
          offsetX = this.offsetX,
          offsetY = this.offsetY;

        if (resize) {
          if (selectType === "x") {
            if (cpIndex === 0) {
              this.startXMin = getPointPos(xAxis, startXMin, dx);
            } else {
              this.startXMax = getPointPos(xAxis, startXMax, dx);
            }
          } else if (selectType === "y") {
            if (cpIndex === 0) {
              this.startYMin = getPointPos(yAxis, startYMin, dy);
            } else {
              this.startYMax = getPointPos(yAxis, startYMax, dy);
            }
          } else {
            this.startXMax = getPointPos(xAxis, startXMax, dx);
            this.startYMax = getPointPos(yAxis, startYMax, dy);
          }
        }

        if (redraw) {
          this.startXMin = getPointPos(xAxis, startXMin, offsetX);
          this.startXMax = getPointPos(xAxis, startXMax, offsetX);
          this.startYMin = getPointPos(yAxis, startYMin, offsetY);
          this.startYMax = getPointPos(yAxis, startYMax, offsetY);

          this.offsetX = 0;
          this.offsetY = 0;
        }
      },
      /**
       * Default formatter of label's content
       * @private
       */
      defaultFormatter: function () {
        return (
          "Min: " +
          this.min +
          "<br>Max: " +
          this.max +
          "<br>Average: " +
          this.average +
          "<br>Bins: " +
          this.bins
        );
      },
      /**
       * Set values for xAxisMin, xAxisMax, yAxisMin, yAxisMax, also
       * when chart is inverted
       * @private
       */
      getExtremes: function (xAxisMin, xAxisMax, yAxisMin, yAxisMax) {
        return {
          xAxisMin: Math.min(xAxisMax, xAxisMin),
          xAxisMax: Math.max(xAxisMax, xAxisMin),
          yAxisMin: Math.min(yAxisMax, yAxisMin),
          yAxisMax: Math.max(yAxisMax, yAxisMin),
        };
      },
      /**
       * Definitions of calculations (min, max, average, bins)
       * @private
       */
      min: function () {
        var min = Infinity,
          series = this.chart.series,
          ext = this.calculations.getExtremes(
            this.xAxisMin,
            this.xAxisMax,
            this.yAxisMin,
            this.yAxisMax
          ),
          isCalculated = false; // to avoid Infinity in formatter

        series.forEach(function (serie) {
          if (
            serie.visible &&
            serie.options.id !== "highcharts-navigator-series"
          ) {
            serie.points.forEach(function (point) {
              if (
                !point.isNull &&
                point.y < min &&
                point.x > ext.xAxisMin &&
                point.x <= ext.xAxisMax &&
                point.y > ext.yAxisMin &&
                point.y <= ext.yAxisMax
              ) {
                min = point.y;
                isCalculated = true;
              }
            });
          }
        });

        if (!isCalculated) {
          min = "";
        }

        return min;
      },
      max: function () {
        var max = -Infinity,
          series = this.chart.series,
          ext = this.calculations.getExtremes(
            this.xAxisMin,
            this.xAxisMax,
            this.yAxisMin,
            this.yAxisMax
          ),
          isCalculated = false; // to avoid Infinity in formatter

        series.forEach(function (serie) {
          if (
            serie.visible &&
            serie.options.id !== "highcharts-navigator-series"
          ) {
            serie.points.forEach(function (point) {
              if (
                !point.isNull &&
                point.y > max &&
                point.x > ext.xAxisMin &&
                point.x <= ext.xAxisMax &&
                point.y > ext.yAxisMin &&
                point.y <= ext.yAxisMax
              ) {
                max = point.y;
                isCalculated = true;
              }
            });
          }
        });

        if (!isCalculated) {
          max = "";
        }

        return max;
      },
      average: function () {
        var average = "";

        if (this.max !== "" && this.min !== "") {
          average = (this.max + this.min) / 2;
        }

        return average;
      },
      bins: function () {
        var bins = 0,
          series = this.chart.series,
          ext = this.calculations.getExtremes(
            this.xAxisMin,
            this.xAxisMax,
            this.yAxisMin,
            this.yAxisMax
          ),
          isCalculated = false; // to avoid Infinity in formatter

        series.forEach(function (serie) {
          if (
            serie.visible &&
            serie.options.id !== "highcharts-navigator-series"
          ) {
            serie.points.forEach(function (point) {
              if (
                !point.isNull &&
                point.x > ext.xAxisMin &&
                point.x <= ext.xAxisMax &&
                point.y > ext.yAxisMin &&
                point.y <= ext.yAxisMax
              ) {
                bins++;
                isCalculated = true;
              }
            });
          }
        });

        if (!isCalculated) {
          bins = "";
        }

        return bins;
      },
    },
  },
  /**
   * A measure annotation.
   *
   * @extends annotations.crookedLine
   * @excluding labels, labelOptions, shapes, shapeOptions
   * @sample highcharts/annotations-advanced/measure/
   *         Measure
   * @product highstock
   * @optionparent annotations.measure
   */
  {
    typeOptions: {
      /**
       * Decides in what dimensions the user can resize by dragging the
       * mouse. Can be one of x, y or xy.
       */
      selectType: "xy",
      /**
       * This number defines which xAxis the point is connected to.
       * It refers to either the axis id or the index of the axis
       * in the xAxis array.
       */
      xAxis: 0,
      /**
       * This number defines which yAxis the point is connected to.
       * It refers to either the axis id or the index of the axis
       * in the yAxis array.
       */
      yAxis: 0,
      background: {
        /**
         * The color of the rectangle.
         */
        fill: "rgba(130, 170, 255, 0.4)",
        /**
         * The width of border.
         */
        strokeWidth: 0,
        /**
         * The color of border.
         */
        stroke: void 0,
      },
      /**
       * Configure a crosshair that is horizontally placed in middle of
       * rectangle.
       *
       */
      crosshairX: {
        /**
         * Enable or disable the horizontal crosshair.
         *
         */
        enabled: true,
        /**
         * The Z index of the crosshair in annotation.
         */
        zIndex: 6,
        /**
         * The dash or dot style of the crosshair's line. For possible
         * values, see
         * [this demonstration](https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-dashstyle-all/).
         *
         * @type    {Highcharts.DashStyleValue}
         * @default Dash
         */
        dashStyle: "Dash",
        /**
         * The marker-end defines the arrowhead that will be drawn
         * at the final vertex of the given crosshair's path.
         *
         * @type       {string}
         * @default    arrow
         */
        markerEnd: "arrow",
      },
      /**
       * Configure a crosshair that is vertically placed in middle of
       * rectangle.
       */
      crosshairY: {
        /**
         * Enable or disable the vertical crosshair.
         *
         */
        enabled: true,
        /**
         * The Z index of the crosshair in annotation.
         */
        zIndex: 6,
        /**
         * The dash or dot style of the crosshair's line. For possible
         * values, see [this demonstration](https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-dashstyle-all/).
         *
         * @type      {Highcharts.DashStyleValue}
         * @default   Dash
         * @apioption annotations.measure.typeOptions.crosshairY.dashStyle
         *
         */
        dashStyle: "Dash",
        /**
         * The marker-end defines the arrowhead that will be drawn
         * at the final vertex of the given crosshair's path.
         *
         * @type       {string}
         * @default    arrow
         * @validvalue ["none", "arrow"]
         *
         */
        markerEnd: "arrow",
      },
      label: {
        /**
         * Enable or disable the label text (min, max, average,
         * bins values).
         *
         * Defaults to true.
         */
        enabled: true,
        /**
         * CSS styles for the measure label.
         *
         * @type    {Highcharts.CSSObject}
         * @default {"color": "#666666", "fontSize": "11px"}
         */
        style: {
          fontSize: "11px",
          color: "#666666",
        },
        /**
         * Formatter function for the label text.
         *
         * Available data are:
         *
         * <table>
         *
         * <tbody>
         *
         * <tr>
         *
         * <td>`this.min`</td>
         *
         * <td>The mininimum value of the points in the selected
         * range.</td>
         *
         * </tr>
         *
         * <tr>
         *
         * <td>`this.max`</td>
         *
         * <td>The maximum value of the points in the selected
         * range.</td>
         *
         * </tr>
         *
         * <tr>
         *
         * <td>`this.average`</td>
         *
         * <td>The average value of the points in the selected
         * range.</td>
         *
         * </tr>
         *
         * <tr>
         *
         * <td>`this.bins`</td>
         *
         * <td>The amount of the points in the selected range.</td>
         *
         * </tr>
         *
         * </table>
         *
         * @type      {function}
         *
         */
        formatter: void 0,
      },
    },
    controlPointOptions: {
      positioner: function (target) {
        var cpIndex = this.index,
          chart = target.chart,
          options = target.options,
          typeOptions = options.typeOptions,
          selectType = typeOptions.selectType,
          controlPointOptions = options.controlPointOptions,
          inverted = chart.inverted,
          xAxis = chart.xAxis[typeOptions.xAxis],
          yAxis = chart.yAxis[typeOptions.yAxis],
          targetX = target.xAxisMax,
          targetY = target.yAxisMax,
          ext = target.calculations.getExtremes(
            target.xAxisMin,
            target.xAxisMax,
            target.yAxisMin,
            target.yAxisMax
          ),
          x,
          y;

        if (selectType === "x") {
          targetY = (ext.yAxisMax - ext.yAxisMin) / 2;

          // first control point
          if (cpIndex === 0) {
            targetX = target.xAxisMin;
          }
        }

        if (selectType === "y") {
          targetX = ext.xAxisMin + (ext.xAxisMax - ext.xAxisMin) / 2;

          // first control point
          if (cpIndex === 0) {
            targetY = target.yAxisMin;
          }
        }

        if (inverted) {
          x = yAxis.toPixels(targetY);
          y = xAxis.toPixels(targetX);
        } else {
          x = xAxis.toPixels(targetX);
          y = yAxis.toPixels(targetY);
        }

        return {
          x: x - controlPointOptions.width / 2,
          y: y - controlPointOptions.height / 2,
        };
      },
      events: {
        drag: function (e, target) {
          var translation = this.mouseMoveToTranslation(e),
            selectType = target.options.typeOptions.selectType,
            index = this.index,
            x = selectType === "y" ? 0 : translation.x,
            y = selectType === "x" ? 0 : translation.y;

          target.resize(x, y, index, selectType);

          target.resizeX += x;
          target.resizeY += y;
          target.redraw(false, true);
        },
      },
    },
  }
);

Annotation.types.measure = Measure;

export default Measure;
